/*
 * Copyright 2010 by BCV (Lausanne, Switzerland)
 * All rights reserved.
 *
 *  Author: ELCA Informatique SA
 *  Version:  $Revision: 1 $
 */

package tcln.gxtx.client.widget.treegrid;

import tcln.gxtx.client.utils.async.DeferredCommandScheduler;

import com.extjs.gxt.ui.client.Style.ScrollDir;
import com.extjs.gxt.ui.client.data.ModelData;
import com.extjs.gxt.ui.client.store.ListStore;
import com.extjs.gxt.ui.client.util.Point;
import com.extjs.gxt.ui.client.widget.grid.ColumnData;
import com.extjs.gxt.ui.client.widget.grid.GridSelectionModel.Cell;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NodeList;
import com.google.gwt.user.client.Command;

/**
 * A special version of a tree gridview which is optimized for <b>fast</b> rendering and response.
 * 
 * @param <M>
 * @author Truong Chau Lien Ngoc (lienngoc@gmail.com)
 */
public class FastTreeGridView<M extends ModelData> extends ExtendedTreeGridView<M> {
    private static final int CACHE_SIZE = 50;

    /**
     * Current scroller's content height. This field is updated with
     * {@link com.extjs.gxt.ui.client.core.El#getHeight(boolean)} (argument = <code>true</code>) when needed.
     */
    private int currentScrollerContentHeight = -1;

    /**
     * Current scroll state to eliminate heavy invocations to DOM to get scroller info as well as to fix an ugly problem
     * of scroller reset in IE (<i><b>Problem</b>: the v-scrollbar is automatically scrolled to top on switching
     * tab</i>). This field is updated when the scroller is updated (i.e. when {@link #syncScroll()} is called).
     */
    private Point currentScrollState;

    /**
     * A flag indicating that method {@link #syncScroll()} should be ignored once. This flag is automatically reset in
     * the next time this method is called.
     */
    private boolean syncScrollDisabledOnce = false;

    /**
     * A flag indicating if {@link #syncScroll()} is currently in active.
     */
    private boolean synchronizingScroll = false;

    private boolean renderWidgetsDisabled = false;
    private boolean renderOnlyVisibleWidgets = false;
    private boolean renderRowsDisabled = false;

    /**
     * A flag indicating that currently this view is refreshing its view (i.e. inside {@link #refreshRow(int)}).
     */
    private boolean refreshingRow = false;

    /**
     * Currently focused cell.
     */
    private Cell focusedCell;

    /**
     * Default constructor.
     */
    public FastTreeGridView() {
        setCacheSize(CACHE_SIZE);
    }

    void resetOptimizationInfo() {
        currentScrollerContentHeight = -1; // reset scroller height
        currentScrollState = null;
        syncScrollDisabledOnce = false;
        renderWidgetsDisabled = false;
        renderOnlyVisibleWidgets = false;
    }

    /**
     * Does synchronize current scroller when this grid view is visible again (e.g. on switching to the container tab
     * panel of this grid view).
     * 
     * @param contentRefreshed
     *            <code>true</code> to force visible rows to be updated (i.e. re-rendered). Using <code>false</code>
     *            only when it's sure that gridview's content is unchanged.
     * @param scrollDir
     *            Scroll direction. If it's <code>null</code>, both directions are considered.
     */
    void syncScrollOnVisible(boolean contentRefreshed, ScrollDir scrollDir) {
        if (currentScrollState != null) {

            // when we reset scroll state, Event.ONSCROLL is fired, which is redundant because we don't intend to change
            // anything. Thus, disable syncScroll() here once.
            syncScrollDisabledOnce = !contentRefreshed;

            // reset scroll state
            if (scrollDir == null || scrollDir == ScrollDir.HORIZONTAL) {
                getScroller().setScrollLeft(currentScrollState.x);
            }
            if (scrollDir == null || scrollDir == ScrollDir.VERTICAL) {
                getScroller().setScrollTop(currentScrollState.y);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void syncScroll() {
        if (syncScrollDisabledOnce) {
            syncScrollDisabledOnce = false;
            return;
        }
        synchronizingScroll = true;
        super.syncScroll();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void doUpdate() {
        super.doUpdate();
        if (synchronizingScroll) {
            currentScrollState = getScrollState();
            synchronizingScroll = false;
        }
    }

    /**
     * Returns the visible row count.
     * <p>
     * This method is optimized so that it only invokes {@link com.extjs.gxt.ui.client.core.El#getHeight(boolean)} once.
     * This current scroller's height is stored in {@link #currentScrollerContentHeight}. This member is reset in
     * {@link AbstractTreeGrid#resetScrollerCache()} normally when switching to another tab (i.e. show another view).
     * 
     * @return visible row count.
     */
    @Override
    protected int getVisibleRowCount() {
        int rowHeight = getStyleRowHeight();

        int visibleHeight = currentScrollerContentHeight;
        if (visibleHeight < 0) {
            currentScrollerContentHeight = scroller.getHeight(true);
            visibleHeight = currentScrollerContentHeight;
        }

        if (visibleHeight < 1) {
            return 0;
        } else {
            return (int) visibleHeight / rowHeight + 1;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected int[] getVisibleRows(int count) {
        int rowHeight = getStyleRowHeight();

        // LNT [Jul 30, 2010]: [optimization]
        // on scrolling, 'currentScrollState' changes -> using it is simpler than looking up from DOM.
        // on switching tab, visible rows are not changed -> use 'currentScrollState' is better.
        int sc;
        if (currentScrollState != null) {
            sc = currentScrollState.y;
        } else {
            sc = scroller.getScrollTop(); // heavy search in DOM but unavoidable
            currentScrollState = new Point(0, sc); // update scroll state initially
        }

        int start = 0;
        if (sc > 0) {
            start = (int) Math.floor(sc / rowHeight) - 1;
        }
        int first = Math.max(start, 0);
        int last = Math.min(first + count + 2, grid.getStore().getCount() - 1);

        return new int[] { first, last };
    }

    void disableRowRendering() {
        renderRowsDisabled = true;
        renderWidgetsDisabled = true;
    }

    void enableRowRendering() {
        renderRowsDisabled = false;
        renderWidgetsDisabled = false;
    }

    void setRenderOnlyVisibleWidgets(boolean renderOnlyVisibleWidgets) {
        this.renderOnlyVisibleWidgets = renderOnlyVisibleWidgets;
    }

    void clearVisibleRowPlaceholders() {
        enableRowRendering();

        int[] vr = getVisibleRows(getVisibleRowCount());
        int startRow = vr[0];
        int endRow = vr[1];

        NodeList<Element> rows = getRows();
        for (int i = startRow; i <= endRow; i++) {
            rows.getItem(i).setInnerHTML("");
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void renderWidgets(int startRow, int endRow) {
        if (!renderWidgetsDisabled) {
            if (renderOnlyVisibleWidgets) {
                int[] visibleRowIndexes = getVisibleRows(getVisibleRowCount());
                if (visibleRowIndexes[0] > startRow) {
                    startRow = visibleRowIndexes[0];
                }
                if (visibleRowIndexes[1] < endRow) {
                    endRow = visibleRowIndexes[1];
                }
            }
            if (startRow <= endRow) {
                super.renderWidgets(startRow, endRow);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected String getRenderedValue(ColumnData data, int rowIndex, int colIndex, ModelData m, String property) {
        if (!renderRowsDisabled) {
            return super.getRenderedValue(data, rowIndex, colIndex, m, property);
        }

        // because row rendering is disabled, this method does nothing and only a place-holder will be created
        return "";
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void refreshRow(int row) {
        refreshingRow = true;
        super.refreshRow(row);
        refreshingRow = false;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void onRemove(ListStore<ModelData> ds, ModelData m, int index, boolean isUpdate) {

        // LNT [Aug 2, 2010]:
        // [optimization]: The GridView.onRemove performs some redundant work
        // 1/ 'constrainFocusElement' takes 1s in IE web-mode => Thus, don't need to do that.
        // 2/ 'applyEmptyText' is useless.

        if (refreshingRow) {
            detachWidget(index, true);
            removeRow(index);
            if (!isUpdate) {
                final int delay = 10;
                DeferredCommandScheduler.scheduleOnce(new Command() {
                    @Override
                    public void execute() {
                        processRows(0, false);
                    }
                }, delay);
            }
        } else {
            super.onRemove(ds, m, index, isUpdate);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void focusCell(int rowIndex, int colIndex, boolean hscroll) {
        if (focusedCell == null || focusedCell.cell != colIndex || focusedCell.row != rowIndex) {
            super.focusCell(rowIndex, colIndex, hscroll);
            if (focusedCell == null) {
                focusedCell = new Cell(rowIndex, colIndex);
            } else {
                focusedCell.cell = colIndex;
                focusedCell.row = rowIndex;
            }
        }
    }
}
